﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Web;

namespace System.Testing
{
    /// <summary>
    /// Service for working with Test Parameters.
    /// Allows to check if parameter was enabled, to check overridens value of parameters, 
    /// to override parameters (handling static event).
    /// Allows to find available test parameters (using TestParameterAttribute and public static field of type TestParameter).
    /// To make it work Overriding event must be handled and IsEnabled flag set to true
    /// </summary>
    public static class TestParametersService
    {
        /// <summary>
        /// Flag defining if overriding is enabled, or real values only should be used
        /// </summary>
        public static bool IsEnabled = true;

        /// <summary>
        /// Check test parameter and returns test value (converted to expected type) if parameter was overriden,
        /// Otherwise return real value
        /// </summary>
        /// <typeparam name="T">Type of parameter value</typeparam>
        /// <param name="testParameterName">name of test parameter</param>
        /// <param name="realValue">real value, will be returned if parameter was not overriden</param>
        /// <returns>real or overriden value of parameter</returns>
        public static T GetOverridableValue<T>(string testParameterName, T realValue)
        {
            if (Overriding == null || !IsEnabled) return realValue;

            var e = new TestParameterOverridingEventArgs(testParameterName);
            e.IsOverriden = false;
            e.OverridenValue = default(T);
            Overriding(realValue, e);
            if (!e.IsOverriden) return realValue;
            if (e.OverridenValue == null) return default(T);
            if (e.OverridenValue is T) return (T)e.OverridenValue;
            return (T)Convert.ChangeType(e.OverridenValue, typeof(T));
        }

        /// <summary>
        /// Check test parameter and returns test value (converted to expected type) if parameter was overriden,
        /// Otherwise return real value
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="testParameterName"></param>
        /// <param name="realValueFunction">function that returns real value</param>
        /// <returns></returns>
        public static T GetOverridableValue<T>(string testParameterName, Func<T> realValueFunction)
        {
            if (Overriding == null || !IsEnabled) return realValueFunction();

            var e = new TestParameterOverridingEventArgs(testParameterName);
            e.IsOverriden = false;
            e.OverridenValue = default(T);
            Overriding(realValueFunction, e);
            if (!e.IsOverriden) return realValueFunction();
            if (e.OverridenValue == null) return default(T);
            if (e.OverridenValue is T) return (T)e.OverridenValue;
            return (T)Convert.ChangeType(e.OverridenValue, typeof(T));
        }

        /// <summary>
        /// Check test parameter and returns test value (converted to expected type) if parameter was overriden,
        /// Otherwise return real value
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="testParameterName"></param>
        /// <param name="valueIsOverriden">returns flag is returned value is overriden</param>
        /// <param name="realValueFunction"></param>
        /// <returns></returns>
        public static T GetOverridableValue<T>(string testParameterName, out bool valueIsOverriden, Func<T> realValueFunction)
        {
            valueIsOverriden = false;
            if (Overriding == null || !IsEnabled) return realValueFunction();

            var e = new TestParameterOverridingEventArgs(testParameterName);
            e.IsOverriden = false;
            e.OverridenValue = default(T);
            Overriding(realValueFunction, e);
            valueIsOverriden = e.IsOverriden;
            if (!e.IsOverriden) return realValueFunction();
            if (e.OverridenValue == null) return default(T);
            if (e.OverridenValue is T) return (T)e.OverridenValue;
            return (T)Convert.ChangeType(e.OverridenValue, typeof(T));
        }

        /// <summary>
        /// Checks if test parameter is overriden
        /// </summary>
        /// <param name="testParameterName">name of parameter</param>
        /// <returns>true if parameter was overriden</returns>
        public static bool IsOverriden(string testParameterName)
        {
            if (Overriding == null || !IsEnabled) return false;

            var e = new TestParameterOverridingEventArgs(testParameterName);
            e.IsOverriden = false;
            Overriding(null, e);
            return e.IsOverriden;
        }

        /// <summary>
        /// Global event responsible for providing overriden values.
        /// Service responsible for storing overriden values should subscribe and handle this event
        /// </summary>
        public static event EventHandler<TestParameterOverridingEventArgs> Overriding;

        static IEnumerable<T> TryCatchEmpty<T>(Func<IEnumerable<T>> valueGetter)
        {
            try
            {
                return valueGetter();
            }
            catch (Exception e)
            {
                Trace.TraceWarning(e.ToString());
                return Enumerable.Empty<T>();
            }
        }

        /// <summary>
        /// Scans all assemblies to find test parameters searching for:
        /// TestParameterAttribute (Assemblies, public Types, public Members),
        /// TestParameter public static fields
        /// </summary>
        /// <returns>list of names of found TestParameters</returns>
        public static IEnumerable<string> FindAllTestParameterNames()
        {

            foreach (var a in TryCatchEmpty(() => AppDomain.CurrentDomain.GetAssemblies()))
            {
                foreach (TestParameterAttribute aa in TryCatchEmpty(() => a.GetCustomAttributes(typeof(TestParameterAttribute), true)))
                    yield return aa.ParameterName;

                foreach (var t in TryCatchEmpty(() => a.GetExportedTypes()))
                {
                    foreach (TestParameterAttribute ta in TryCatchEmpty(() => t.GetCustomAttributes(typeof(TestParameterAttribute), true)))
                        yield return ta.ParameterName;

                    foreach (var m in TryCatchEmpty(() => t.GetMembers()))
                    {
                        var fld = m as System.Reflection.FieldInfo;
                        if (fld != null && fld.IsStatic && fld.FieldType == typeof(TestParameter))
                        {
                            var testParam = fld.GetValue(null) as TestParameter;
                            yield return testParam.Name;
                        }

                        foreach (TestParameterAttribute ma in TryCatchEmpty(() => m.GetCustomAttributes(typeof(TestParameterAttribute), true)))
                            yield return ma.ParameterName;
                    }
                }
            }

            //var assemblyAttrs =
            //    from a in AppDomain.CurrentDomain.GetAssemblies()
            //    from aa in a.GetCustomAttributes(typeof(TestParameterAttribute), true)
            //    select aa as TestParameterAttribute;

            //var typeAttrs = 
            //    from a in AppDomain.CurrentDomain.GetAssemblies()
            //    from t in a.GetExportedTypes()
            //    from ta in t.GetCustomAttributes(typeof(TestParameterAttribute),true)
            //    select ta as TestParameterAttribute
            //    ;

            //var memberAttrs =
            //    from a in AppDomain.CurrentDomain.GetAssemblies()
            //    from t in a.GetExportedTypes()
            //    from m in t.GetMembers()
            //    from ma in m.GetCustomAttributes(typeof(TestParameterAttribute), true)
            //    select ma as TestParameterAttribute
            //    ;

            //return assemblyAttrs.Concat(typeAttrs).Concat(memberAttrs);
        }

        static Lazy<IEnumerable<string>> cachedTestParametersNames
            = new Lazy<IEnumerable<string>>(() => FindAllTestParameterNames().Distinct().ToArray(), true);

        /// <summary>
        /// Get list using FindAllTestParameterNames() and caches it, returns cached value
        /// </summary>
        /// <returns>cached list of found test parameters</returns>
        public static IEnumerable<string> GetAllTestParameterNames()
        {
            return cachedTestParametersNames.Value;
        }
    }

    /// <summary>
    /// Attribute that allows for TestParameterService to find available test parameters in assemblies
    /// </summary>
    public class TestParameterAttribute : Attribute
    {
        public TestParameterAttribute(string testParameterName)
        {
            this.ParameterName = testParameterName;
        }
        public string ParameterName { get; set; }
    }

    /// <summary>
    /// Test parameter class - simplify access to TestParameterService.
    /// Developer creates public static readonly field, and then address to this object for retrieving test parameter value.
    /// This field will be found by TestParameterService as available test parameter
    /// </summary>
    public class TestParameter
    {
        public TestParameter(string name)
        {
            this.Name = name;
        }

        /// <summary>
        /// Name of test parameter
        /// </summary>
        public string Name { get; protected set; }

        /// <summary>
        /// Check if parameter is overriden
        /// </summary>
        public bool IsOverriden { get { return TestParametersService.IsOverriden(this.Name); } }

        /// <summary>
        /// Retrieve test value (converted to T) if parameter is overriden, otherwise returns real value specified
        /// </summary>
        /// <typeparam name="T">expected type of value</typeparam>
        /// <param name="realValueGetter">function generating real value</param>
        /// <returns>test value if parameter is overriden, otherwise returns real value</returns>
        public T GetOverridableValue<T>(Func<T> realValueGetter)
        {
            return TestParametersService.GetOverridableValue(Name, realValueGetter);
        }

        /// <summary>
        /// Retrieve test value (converted to T) if parameter is overriden, otherwise returns real value specified
        /// </summary>
        /// <typeparam name="T">expected type of value</typeparam>
        /// <param name="realValue">real value</param>
        /// <returns>test value if parameter is overriden, otherwise returns real value</returns>
        public T GetOverridableValue<T>(T realValue)
        {
            return TestParametersService.GetOverridableValue(Name, realValue);
        }
    }

    /// <summary>
    /// Event argument for handling overriding event of test parameters
    /// </summary>
    public class TestParameterOverridingEventArgs : EventArgs
    {
        public TestParameterOverridingEventArgs(string name)
        {
            this.Name = name;
        }
        public string Name { get; protected set; }
        public bool IsOverriden { get; set; }
        public object OverridenValue { get; set; }
    }
}